# $Id: scm.py 7557b5d4d82b 2012/12/02 09:07:16 pgurumur $
# Copyright (c) 2009 Prabhu Gurumurthy <pgurumur@gmail.com>
# 
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#       

import os
import shutil
import tempfile

import pysvn

from djanet.lib import exception, log, stdlib

__version__ = "$Revision: 7557b5d4d82b $"
__author__ = "$Author: pgurumur $"

# Multiple exception classes for failures
class SVNErrorNotWC(Exception):
   pass

class SVNErrorAuthFailed(Exception):
   pass

class SVNErrorNotInRepo(Exception):
   pass

class SVNErrorMethodNotAllowed(Exception):
   pass

class SVNAllreadyControlled(Exception):
   pass

# Key value pairs of errors that can be encountered by svn
SVNErrors = {
      150002 : SVNAllreadyControlled,

      # Path is not a working copy
      155007 : SVNErrorNotWC,

      # Authorisation failed
      170001 : SVNErrorAuthFailed,

      # url not exists
      170000 : SVNErrorNotInRepo,
      # method not allowed -already in repo when mkdir
      175002 : SVNErrorMethodNotAllowed
      }

def handle(function, *args, **keywords):
   if function:
      try:
         return function(*args, **keywords)
      except KeyboardInterrupt:
         pass
      except pysvn.ClientError, err:
         for message, code in err.args[1]:
            message += '\n' + str(args) + str(keywords)
            if code in SVNErrors:
               raise SVNErrors[code], message
            else:
               raise

def GetWorkingDir():
   tmpdir = tempfile.gettempdir()
   path = os.path.join(tmpdir, os.path(__file__))
   if not os.path.isdir(path):
      try:
         os.makedirs(path)
         print "creating SVN working copy directory: %s" %path
      except (IOError, OSError):
         raise

   return path

def Rebuild(URL, **Credentials):
   path = GetWorkingDir()
   shutil.rmtree(path)
   path = GetWorkingDir()
   scm = None
   try:
      svn = scm(Credentials)
   except SVNError, message:
      raise
   else:
      svn.checkout(path, URL)

class SVNWorkingCopy(object):
   def __init__(self, Name, URL, **Credentials):
      self._err = exception.SCMError()
      if url is None or len(url) == 0:
         raise ValueError, "SVN url is not defined."

      self._name = Name
      self._isNew = False
      self._svn = None
      try:
         self._svn = scm(Credentials)
      except exception.SCMError, message:
         self._error(message)

      temp = GetWorkingDir()
      destPath = os.path.join(temp, Name)
      destURL = "/".join((URL, Name))

      if not os.path.exists(destPath):
         self._svn.WorkingCopy(destPath, destURL)

      # now for a book file
      self._path = os.path.join(destPath, Name)
      if not os.path.exists(self._path):
          self._isNew = True

   def GetRevision(self, Content = None):
      rev = None
      idLine = None

      if Content:
         try:
            idLine = Content.split("\n")[0]
         except:
            idLine = Content
      elif self._path:
         if os.path.exists(self._path):
            fh = open(self._path)
            idLine = fh.readline().strip()
            fh.close()

      try:
         rev = idLine.split(" ")[2]
      except:
         self._error("cannot parse SVN id string - format incorrect: " + idLine)

      return rev

   def _error(self, Message):
      if Message:
         raise ValueError, Message

   def CompareRevisions(self, Content):
      mesg = None
      rev = self.GetRevision(Content)
      wcrev = self.GetRevision()

      if rev != wcrev:
         mesg = "Cannot update content with an older or unversion edition. "
         mesg += "Download the recent version before making changes."
         mesg += "\nActual revsion: %s, uploaded %s" %(wcrev, rev)

      return mesg

   def CheckIn(self, content, Comment):
      if self._isNew or not content.startswith("$"):
         content += "$Id: scm.py 7557b5d4d82b 2012/12/02 09:07:16 pgurumur $\n"

      fh = None
      try:
         fh = open(self._path, "w")
      except IOError, message:
         self._error(message)
      else:
         fh.write(content)
         fh.close()

      directory = os.path.dirname(self._path)
      if self._isNew:
         self._svn.add(self._path)
         self._svn.propset("svn:keywords", "Id", self._path)

      self._svn.checkin(directory, Comment)


   def Update(self):
      directory = os.path.dirname(self._path)
      changes = self._svn.status(directory, True, False, True)
      for ch in changes:
         if ch.text_status == pysvn.wc_status_kind.conflicted:
            self._log("Working copy %s in conflict", ch.path)
            self._svn.revert(ch.path)
            self._svn.update(ch.path)

         if ch.repos_text_status == wc_status_kind.modified:
            self._svn.update(ch.path)

   def GetCurrent(self):
      res = None

      fh = None
      try:
         fh = file(self._path)
         res = fh.read()
      except IOError, message:
         pass

      if fh is not None:
         fh.close()

      return res

   def Remove(self):
      directory = os.path.dirname(self._path)
      shutil.rmtree(directory)

# Subversion class
class svn(object):
   def __init__(self, **Keywords):
      super(svn, self).__init__()
      """
      constructor takes key value pairs as optional arguments
      example:
         scm = svn()
         or
         scm = svn(username = "user", password = "password", log = "logfile")
         or
         scm = svn(username = "user", password = "password")
      """

      # Instantiate the error class
      self._err = exception.SCMError()

      # SVN client object
      self._client = None

      # Username and password vars
      self._username = None
      self._password = None

      if "username" in Keywords:
         self._username = Keywords["username"]

      if "password" in Keywords:
         self._password = Keywords["password"]

      if "log" in Keywords:
         if bool(Keywords["log"]) is True:
            self._log = log.StreamLog(stdlib.my_name(self))
         else:
            self._log = log.NullLog(stdlib.my_name(self))

      self._log_message = "default upload"
      self._revUpdateComplete = None
      self.loginCount = 0

   # Instantiate the pysvn client object with all the necessary callbacks
   def Client(self):
      self._client = pysvn.Client()
      self._client.exception_style = 1
      self._client.callback_get_login = self._GetLogin
      self._client.callback_get_log_message = self._GetLogMessage
      self._client.callback_notify = self._Notify
      self._client.callback_cancel = self._Cancel
      self._client.callback_ssl_client_cert_password_prompt = \
            self._CallbackSSLPasswordPrompt
      self._client.callback_ssl_client_cert_prompt = \
            self._CallbackSSLClientPrompt
      self._client.callback_ssl_server_prompt = self._CallbackSSLServerPrompt
      self._client.callback_ssl_server_trust_prompt = \
            self._CallbackSSLServerTrustPrompt

   def _error(self, Message):
      if Message:
         raise exception.SCMError, Message

   def _GetLogin(self, Realm, User, Save):
      if self._username:
         self.loginCount += 1
         if self.loginCount > 10:
            return False, '', '', False

      if self._username:
         return True, self._username, self._password, False
      else:
         return False, '', '', False

   def CheckIn(self, File, Comment, New = False):
      if os.path.exists(File):
         if New:
            self.add(File)
            self.propset("svn:keywords", "Id", File)

         self.checkin(File, Comment)

   def Update(self, Path):
      retval = None
      temp = None
      changes = self.status(Path, True, False, True)
      if changes:
         for ch in changes:
            if ch.text_status == pysvn.wc_status_kind.conflicted:
               self.revert(ch.path)
               temp = self.update(ch.path)

            if ch.repos_text_status == pysvn.wc_status_kind.modified:
               temp = self.update(ch.path)
      else:
         temp = self.update(Path)
         retval = temp[0].number

      return int(retval)

   def _GetLogMessage(self):
      return True, self._log_message

   def _Cancel(self):
      return False

   def _Notify(self, ArgDict):
      if ArgDict['action'] == pysvn.wc_notify_action.update_completed:
         self._revUpdateComplete = ArgDict['revision']
      elif ArgDict['path']:
         msg = 'svn: %s %s' % (ArgDict['action'], ArgDict['path'])
         self._log(msg)

   def exists(self, URL):
      retval = True
      if URL:
         try:
            handle(self._client.info2, URL)
         except :
            retval = False

      return retval

   def WorkingCopy(self, Path, Url):
      if Path and URL:
         try:
            handle(self._client.mkdir, URL, "new directory")
         except SVNErrorMethodNotAllowed:
            pass
         except SVNErrorNotInRepo:
            raise
         else:
            self.checkout(Path, URL)

   def checkout(self, Path, URL):
      retval = 0
      if Path and URL:
         self._revUpdateComplete = None
         try:
            handle(self._client.checkout, URL, Path, recurse=True)
         except SVNErrorNotInRepo, message:
            self._error(message)
         except SVNErrorAuthFailed, message:
            self._error(message)
         except SVNErrorNotWC, message:
            self._error(message)
         else:
            if self._revUpdateComplete is not None:
               retval = self._revUpdateComplete.number

      return retval

   def add(self, Path):
     if Path:
        self._client.add(Path, recurse = True, force = False)

   def checkin(self, Path, Mesg = None):
      retval = None
      if Path:
         msg = None
         if Mesg is None:
            ix, msg = self._GetLogMessage()
         else:
            msg = Mesg

         rev = self._client.checkin(Path, msg, recurse = True)
         if rev is None:
            retval = 0
         elif rev.number > 0:
            retval = rev.number
         else:
            self._error("SVN commit failed")

      return retval

   def update(self, Path):
      return self._client.update(Path, recurse = True)

   def list(self, Path):
      return [ix[0] for ix in self._client.list(Path, recurse = True)]

   def info2(self, Path):
      return self._client.info2(Path, recurse = True)

   def propset(self, Name, Value, Path):
      self._client.propset(Name, Value, Path, recurse = False)

   def remove(self, PathList):
      self._client.remove(PathList, force = True)

   def revert(self, Path):
      return self._client.revert(Path)

   def status(self, Path, recurse = True, getall = True, update = False):
      return self._client.status(Path, recurse, getall, update)

   def _CallbackSSLPasswordPrompt(self):
      pass

   def _CallbackSSLClientPrompt(self):
      pass

   def _CallbackSSLServerPrompt(self):
      pass

   def _CallbackSSLServerTrustPrompt(self, Data):
      return True, Data['failures'], True
